// @name Advanced Settings Dialog
// @description Adds an "Advanced Settings" menu item to configure various Firefox settings via a dialog.
// @author (Original author), Gemini
['userChrome.enableRightClickCloseTab', '右键关闭标签页 (CTRL+右键弹出菜单)'],
['userChrome.enableRightClickUndoTab', '右键标签栏撤销关闭 (CTRL+右键弹出菜单)'],
['browser.tabs.closeTabByDblclick', '双击关闭标签页'],
['browser.tabs.closeWindowWithLastTab', '关闭最后一个标签页时关闭窗口'],
['browser.urlbar.openintab', '地址栏在新标签页打开'],
['browser.tabs.loadBookmarksInTabs', '书签栏在新标签页打开'],
['browser.search.openintab', '搜索框在新标签页打开'],
['browser.urlbar.trimURLs', '地址栏隐藏 http/https'],
['browser.urlbar.scotchBonnet.enableOverride', '启用下拉选择搜索引擎'],
['browser.tabs.insertAfterCurrent', '在当前标签页右侧打开新标签页'],
['browser.newtabpage.activity-stream.improvesearch.handoffToAwesomebar', '主页搜索框输入时跳转到地址栏'],
['browser.bookmarks.openInTabClosesMenu', '禁用书签菜单 CTRL+多次点击打开多个书签'],
['browser.chrome.toolbar_tips', '鼠标悬停小提示'],
['browser.tabs.hoverPreview.enabled', '启用标签页缩略图'],
['browser.compactmode.show', '定制密度增加紧凑模式'],
['userChrome.hideWindowControls', '隐藏窗口控制按钮'],
['userChrome.useSerifFont', '界面使用衬线字体']
// --- Global State & Cached Elements ---
const cleanupFunctions = new Set();
// --- Preference Handling ---
const getPref = (k, d = false) => Services.prefs.getBoolPref(k, d);
const setPref = (k, v) => Services.prefs.setBoolPref(k, v);
const resetPrefs = () => SETTINGS.forEach(([k]) => Services.prefs.clearUserPref(k));
// --- Centralized Update Handlers ---
'userChrome.enableRightClickCloseTab': setupRightClickHandler,
'userChrome.enableRightClickUndoTab': setupRightClickHandler,
'userChrome.hideWindowControls': applyStyle,
'userChrome.useSerifFont': applyStyle
styleEl = document.createElement('style');
document.head.appendChild(styleEl);
cleanupFunctions.add(() => styleEl?.remove());
padding: 15px; border: 1px solid #0004; border-radius: 4px;
background: -moz-Dialog; color: var(--main-color, #000);
min-width: 400px; max-width: 600px; z-index: 10000;
font: message-box; font-size: 14px; line-height: 14px;
box-shadow: 0 1px 4px #0003;
#adv-settings-dialog::backdrop { background: #0002; }
/* New style for the header container */
justify-content: space-between;
margin-bottom: 15px; /* Maintain gap between header and form */
#adv-settings-dialog h2 {
margin: 0; /* Remove default margin as container handles spacing */
.adv-settings-form { display: flex; flex-direction: column; gap: 12px; }
.setting-item { display: flex; align-items: center; gap: 8px; }
.setting-label { flex: 1; cursor: pointer; }
.button-row { display: flex; justify-content: flex-end; gap: 8px; margin-top: 20px; }
${getPref('userChrome.hideWindowControls') ? '.titlebar-buttonbox-container { display: none !important; }' : ''}
${getPref('userChrome.useSerifFont') ? '* { font-family: "serif" !important; }' : ''}
// --- Dynamic Event Handling (Right Click) ---
function handleTabContainerRightClick(e) {
if ( !== 2 || e.ctrlKey) return; // Only for right-click without CTRL const tab = e.target.closest('.tabbrowser-tab');
if (getPref('userChrome.enableRightClickCloseTab') && tab) {
e.preventDefault(); e.stopPropagation();
gBrowser[tab.multiselected ? 'removeMultiSelectedTabs' : 'removeTab'](tab, { animate: true, triggeringEvent: e });
} else if (getPref('userChrome.enableRightClickUndoTab') && !tab) {
if (SessionStore.getClosedTabCount(window) > 0) {
e.preventDefault(); e.stopPropagation();
SessionStore.undoCloseTab(window, 0);
function setupRightClickHandler() {
const container = gBrowser?.tabContainer;
// Remove existing listener to prevent duplicates
container.removeEventListener('contextmenu', handleTabContainerRightClick, true);
// Add listener only if at least one feature is enabled
if (getPref('userChrome.enableRightClickCloseTab') || getPref('userChrome.enableRightClickUndoTab')) {
container.addEventListener('contextmenu', handleTabContainerRightClick, true);
// --- UI Creation and Management ---
function createAndShowDialog() {
// Create dialog only once
dialogEl = document.createElement('dialog');
cleanupFunctions.add(() => dialogEl?.remove());
const dialogClickHandler = e => {
const rect = dialogEl.getBoundingClientRect();
dialogEl.addEventListener('click', dialogClickHandler);
cleanupFunctions.add(() => dialogEl.removeEventListener('click', dialogClickHandler));
// New header container for title and reset button
const headerContainer = document.createElement('div');
headerContainer.className = 'dialog-header';
dialogEl.appendChild(headerContainer);
const titleEl = document.createElement('h2');
titleEl.textContent = '高级设置';
headerContainer.appendChild(titleEl);
const resetBtn = document.createElement('button');
resetBtn.textContent = '重置全部';
headerContainer.appendChild(resetBtn); // Append to headerContainer
const resetHandler = () => {
// Refresh UI after prefs are cleared
requestAnimationFrame(() => refreshDialogUI());
resetBtn.addEventListener('click', resetHandler);
cleanupFunctions.add(() => resetBtn.removeEventListener('click', resetHandler));
// --- Helper to create a single setting item ---
const createCheckboxItem = (pref, labelText) => {
const div = document.createElement('div');
div.className = 'setting-item';
const checkbox = document.createElement('input');
checkbox.checked = getPref(pref);
const changeHandler = () => {
setPref(pref, checkbox.checked);
PREF_HANDLERS[pref]?.(); // Trigger specific handler if exists
checkbox.addEventListener('change', changeHandler);
cleanupFunctions.add(() => checkbox.removeEventListener('change', changeHandler));
const label = document.createElement('label');
label.htmlFor = checkbox.id;
label.className = 'setting-label';
label.textContent = labelText;
div.append(checkbox, label);
const form = document.createElement('form');
form.className = 'adv-settings-form';
SETTINGS.forEach(([pref, label]) => form.appendChild(createCheckboxItem(pref, label)));
dialogEl.appendChild(form);
document.body.appendChild(dialogEl);
// --- Refresh UI state before showing ---
const refreshDialogUI = () => {
SETTINGS.forEach(([pref]) => {
const cb = dialogEl.querySelector(`#pref-${CSS.escape(pref)}`);
if (cb) cb.checked = getPref(pref);
// Also refresh styles and listeners that depend on these settings
setupRightClickHandler();
// --- Menu Item Integration ---
const appMenu = document.getElementById('appMenu-popup');
const settingsBtn = document.getElementById('appMenu-settings-button');
if (!appMenu || !settingsBtn || document.getElementById('appMenu-advanced-settings-button')) {
const btn = document.createXULElement('toolbarbutton');
= 'appMenu-advanced-settings-button'; btn.className = 'subviewbutton';
btn.setAttribute('label', '高级设置');
const commandHandler = e => {
btn.addEventListener('command', commandHandler);
cleanupFunctions.add(() => btn.removeEventListener('command', commandHandler));
settingsBtn.insertAdjacentElement('afterend', btn);
function addMenuItemOnFirstOpen() {
const appMenuPopup = document.getElementById('appMenu-popup');
if (!appMenuPopup) return;
const onMenuShowing = () => {
appMenuPopup.removeEventListener('popupshowing', onMenuShowing);
appMenuPopup.addEventListener('popupshowing', onMenuShowing);
cleanupFunctions.add(() => appMenuPopup.removeEventListener('popupshowing', onMenuShowing));
// --- Initialization and Cleanup ---
// Unregister preference observers
for (const [pref, handler] of Object.entries(PREF_HANDLERS)) {
Services.prefs.removeObserver(pref, handler);
// Remove the global right-click listener
gBrowser?.tabContainer.removeEventListener('contextmenu', handleTabContainerRightClick, true);
// Run all registered cleanup functions
cleanupFunctions.forEach(fn => {
try { fn(); } catch (e) { console.error('Cleanup function failed:', e); }
cleanupFunctions.clear();
for (const [pref, handler] of Object.entries(PREF_HANDLERS)) {
Services.prefs.addObserver(pref, handler);
handler(); // Run once on startup
// Wait for the DOM to be ready before inserting the menu item
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', addMenuItemOnFirstOpen, { once: true });
addMenuItemOnFirstOpen();
window.addEventListener('unload', cleanup, { once: true });